package core.db;

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Set;

import com.amazonaws.auth.PropertiesCredentials;
import com.amazonaws.services.simpledb.AmazonSimpleDB;
import com.amazonaws.services.simpledb.AmazonSimpleDBClient;
import com.amazonaws.services.simpledb.model.Attribute;
import com.amazonaws.services.simpledb.model.BatchPutAttributesRequest;
import com.amazonaws.services.simpledb.model.DeleteAttributesRequest;
import com.amazonaws.services.simpledb.model.Item;
import com.amazonaws.services.simpledb.model.ReplaceableAttribute;
import com.amazonaws.services.simpledb.model.ReplaceableItem;
import com.amazonaws.services.simpledb.model.SelectRequest;

import core.network.PingRequest;
import core.network.marshal.MarshalUtils;
import core.servlets.HomeServlet;
import core.util.IpPortPair;
import core.util.StoredData;
import core.util.Utilities;

/**
 * Database communication
 * 
 * @author hb
 * 
 */
public class DBWrapper {
	private static final String domainName = "groupmembership";

	private static AmazonSimpleDB sdb;

	public static final double INTERVAL = 60000;

	private HomeServlet servlet;

	public DBWrapper(HomeServlet servlet) throws IOException {
		sdb = new AmazonSimpleDBClient(new PropertiesCredentials(
				DBWrapper.class
						.getResourceAsStream("AwsCredentials.properties")));

		this.servlet = servlet;
		insertDBRow(InetAddress.getLocalHost().getHostAddress(), servlet
				.getOutSocket().getLocalPort(), servlet.getServerId());
	}

	/**
	 * Server calls this every X minutes.
	 */
	public void updateGroupMembership() {
		System.out.println("Now Sync to DB...");
		HashMap<String, IpPortPair> dbTable = queryDB();

		InetAddress IP = servlet.getIP();
		int port = servlet.getServletPort();
		String serverId = servlet.getServerId();

		// 1. add yourself to simpleDB is not there
		if (!dbTable.containsKey(serverId)) {
			dbTable.put(serverId, new IpPortPair(IP, port));
		}
		// 2. get random server id
		if (dbTable.keySet().size() <= 1) {
			servlet.setGroupMembership(dbTable);
			return;
		} else {
			String targetServerId = "";
			do {
				targetServerId = getRandomServerId(dbTable.keySet());
			} while (targetServerId.equals(serverId));

			// Send a ping request to target pair
			StoredData.pingResponse.put(dbTable.get(targetServerId), false);
			sendPingRequest(targetServerId, dbTable.get(targetServerId));
			long startTime = System.currentTimeMillis();
			long timeElapsed = 0;
			do {
				timeElapsed = System.currentTimeMillis() - startTime;
			} while (StoredData.pingResponse.get(dbTable.get(targetServerId))
					.booleanValue() == false
					&& timeElapsed <= Utilities.PING_TIMEOUT);
			if (timeElapsed > Utilities.PING_TIMEOUT) {
				// timeout
				handleTimeout(targetServerId, dbTable);
			} else {
				servlet.setGroupMembership(dbTable);
				return;
			}
		}
	}

	private void sendPingRequest(String targetServerId, IpPortPair pair) {
		PingRequest req = new PingRequest();
		byte[] reqStream = null;
		try {
			reqStream = MarshalUtils.objectToByteArray(req);
		} catch (IOException e1) {
			e1.printStackTrace();
		}
		if (reqStream == null) {
			System.err.println("Problems converting request to byte array");
			return;
		}
		try {
			servlet.getOutSocket().send(
					new DatagramPacket(reqStream, reqStream.length, pair
							.getIp(), pair.getPort()));
			System.out.println("Ping request sent.");
		} catch (IOException e) {
			System.out.println("Send Ping request failed.");
		}
	}

	private static String getRandomServerId(Set<String> serverIdSet) {
		int number = new SecureRandom().nextInt(serverIdSet.size());
		int i = 0;
		for (String obj : serverIdSet) {
			if (i == number)
				return obj;
			i = i + 1;
		}
		return null;
	}

	private static HashMap<String, IpPortPair> queryDB() {
		String selectExpression = "select * from `" + domainName + "`";
		SelectRequest selectRequest = new SelectRequest(selectExpression);
		HashMap<String, IpPortPair> tempMap = new HashMap<String, IpPortPair>();
		for (Item item : sdb.select(selectRequest).getItems()) {
			String serverId = item.getName();
			String IP = null;
			int port = 0;
			for (Attribute attribute : item.getAttributes()) {
				if (attribute.getName().equals("ip")) {
					IP = attribute.getValue();
				} else {
					port = Integer.parseInt(attribute.getValue());
				}
			}
			try {
				tempMap.put(serverId, new IpPortPair(InetAddress.getByName(IP),
						port));
			} catch (UnknownHostException e) {
				e.printStackTrace();
			}
		}
		return tempMap;
	}

	private static void deleteDBRow(String serverId) {
		sdb.deleteAttributes(new DeleteAttributesRequest(domainName, serverId));
	}

	private void insertDBRow(String IP, int port, String serverId) {
		List<ReplaceableItem> sampleData = new ArrayList<ReplaceableItem>();
		sampleData
				.add(new ReplaceableItem(serverId).withAttributes(
						new ReplaceableAttribute("ip", IP, true),
						new ReplaceableAttribute("port",
								Integer.toString(port), true)));
		sdb.batchPutAttributes(new BatchPutAttributesRequest(domainName,
				sampleData));
	}

	public void handleTimeout(String targetServerId,
			HashMap<String, IpPortPair> dbTable) {
		System.out.println("Timeout at server : " + targetServerId);
		deleteDBRow(targetServerId);
		dbTable.remove(targetServerId);
		servlet.setGroupMembership(dbTable);
	}
}
